Skip to content

chore: 工程门禁 — ESLint / Prettier / 覆盖率 / Dependabot / CI 矩阵 / e2e 冒烟 / 依赖升级 - #376

Open
oratis wants to merge 15 commits into
claude/opt-web-uifrom
claude/opt-tooling
Open

chore: 工程门禁 — ESLint / Prettier / 覆盖率 / Dependabot / CI 矩阵 / e2e 冒烟 / 依赖升级#376
oratis wants to merge 15 commits into
claude/opt-web-uifrom
claude/opt-tooling

Conversation

@oratis

@oratis oratis commented Sep 7, 2026

Copy link
Copy Markdown
Owner

第 8 / 8 条,实现 T-4 与 T-5。基于 #375

门禁(T-4)

6 万行 TypeScript 此前没有 lint、没有 formatter、没有覆盖率、没有依赖机器人,PR CI 只跑 Node 一个表面。

  • ESLint 扁平配置 + typescript-eslint 类型感知规则(no-floating-promisesno-misused-promises 等),配合 no-emptyno-consoleeqeqeqprefer-const。现存违规进 warn 基线(69 条),新代码必须在 error 级通过
  • Prettier + EditorConfig,检查范围限定为相对 main 的改动文件,避免本 PR 变成一次全仓库重排。
  • c8 覆盖率,对计费、账户、OTP、会话认证、能力、Soul store 设阈值(先测量再取 min(85, 实测)),CI 上传 lcov 并写入 job summary。
  • Dependabot(npm 周更并合并 minor/patch、Actions 月更、Swift 月更)与 npm audit --omit=dev --audit-level=high 门禁。
  • CI 矩阵与路径触发:核心 job 跑 Node 20/22/24;website/** 触发 Astro 构建;packaging/mac-client/** 触发 macOS runner 上的 swift build;packaging/ios-companion/** 触发模拟器测试。此前后三者只在发布流水线里验证。
  • Playwright 冒烟:完全离线、零模型调用。用 soul store 的 API 合成一个已出生的 soul,再用一个 Anthropic 兼容的桩服务(ANTHROPIC_BASE_URL)分别返回 401 或预设的流式响应,覆盖密钥面板、主外壳、主题持久化、新建会话、三个断点下的布局、出生失败与成功两条路径。

依赖(T-5)

  • npm audit fix + 全部 minor/patch 更新
  • @anthropic-ai/sdk 0.92 → 0.124(落后 32 个 minor),并删掉当初为旧类型添加的断言
  • openai 6 → 7
  • TypeScript 保持 5.x:7.0 会让 linter 无法工作,收益不抵成本,已在提交信息中记录原因

集成修复

最后一个提交处理八条流合并后才出现的 37 个 lint 错误——每条流在自己分支上都是绿的,但基线是在看不到其他七条流新文件的树上算出来的。其中把 cmdBillingReconcilesrc/billing/ 移到 src/cli/:它除了调用 reconcileOnce() 之外全部工作都是打印,而会打印的库代码无法被请求处理器或定时器调用——这正是 src/billing 保持 no-console: error 的理由。

集成后的完整验证

检查 结果
typecheck / typecheck:client 通过
lint 0 error,69 warning(全部为基线条目)
check:api-contract 通过
npm test 1,951 通过 / 0 失败 / 1 跳过(基线 1,645)
build 通过,dist 无测试文件泄漏
website 12 页
macOS swift build(debug + release) 通过,0 警告(此前 14)
iOS 模拟器测试 44 通过 / 0 失败(此前 29)

🤖 Generated with Claude Code

oratis and others added 12 commits September 7, 2026 12:22
…baseline

T-4 (engineering gates): 60k lines of TypeScript had no linter. This adds
ESLint 10 + typescript-eslint 8 (recommended) plus the type-aware rules that
catch real bugs here — no-floating-promises, no-misused-promises,
await-thenable, no-unnecessary-type-assertion — and the house rules no-empty
(allowEmptyCatch: false), no-console (off for src/cli.ts, src/cli/**,
src/log.ts, scripts/**, tests), eqeqeq (null: ignore — `== null` is the
idiomatic nullish check throughout) and prefer-const. Scripts: `npm run lint`
(errors fail) and `npm run lint:fix`; CI runs lint after typecheck.

Type info comes from a dedicated tsconfig.eslint.json via parserOptions.project
instead of parserOptions.projectService: projectService only discovers files
that some tsconfig.json includes, and tsconfig.json (the build config, also run
verbatim by scripts/build-release.sh) excludes every *.test.ts. Its escape
hatch (allowDefaultProject) is capped at a handful of files and forbids `**`
globs, so it cannot carry 190+ test files. (The tests also do not typecheck
under noUncheckedIndexedAccess today, so folding them into tsconfig.json would
break `npm run typecheck`.)

Result today: `npm run lint` → 0 errors, 90 warnings. Warnings by rule:
  43 no-console
  16 @typescript-eslint/no-unnecessary-type-assertion
   9 @typescript-eslint/no-explicit-any
   6 no-empty
   6 @typescript-eslint/no-misused-promises
   6 no-useless-assignment
   2 prefer-const
   1 no-useless-escape
   1 unused eslint-disable directive (src/soul/birth.ts)
Every warning is a pre-existing violation inside a file another work stream
is editing right now (server.ts, lisa-*.ts, birth.ts, cli*, billing/**, …)
or a console.* call whose migration to log.ts is a behaviour change. Those
are pinned to `warn` per file by the `baseline` table at the top of
eslint.config.js — shrink it, never grow it.

Mechanical fixes in files no other stream owns, verified by typecheck + the
full suite: 210 unnecessary `as`/`!` assertions removed (eslint --fix, type
program identical to tsc's), 9 empty catch blocks documented, 4 rethrows now
carry `{ cause }`, 13 unused imports/locals dropped, one `void`-wrapped
setInterval callback, one useless-escape and one regex-spaces fix, a
zero-width space in a doc comment replaced with `<project>`, and three stale
eslint-disable directives removed. No runtime behaviour changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit e9c4b388ac9223b86211d64d5610f880c3d0c0b4)
…gate

T-4 (engineering gates): 60k lines of TypeScript had no formatter, so style was
whatever the last editor's habits were and every PR carried unrelated whitespace
noise.

Config matches what the codebase already does, verified by sampling rather than
assumed: 2-space indent (475 two-space vs 24 three-space leading runs across
agent.ts / soul/store.ts / web/capabilities.ts / providers/anthropic.ts), double
quotes (2,255 double-quoted vs 72 single-quoted literals), semicolons, trailing
commas. printWidth 100: churn at 80/100/120 on five representative files is
176/200/287 changed lines — a wash between 80 and 100 — and 100 matches the
observed p99 line length of 111 while keeping the existing long call signatures
on one line.

Deliberately NOT reformatting the repo. `prettier --check .` reports ~480 files
today; rewriting them in one commit would explode every in-flight branch and
destroy `git blame` on the whole tree. Instead `npm run format:check` (scripts/
format-check.mjs) diffs against the merge-base with the target branch —
$FORMAT_BASE_REF, else origin/<GITHUB_BASE_REF or main>, else that branch — and
only gates files this branch actually touched, plus the working tree and
untracked files so `npm run format` fixes what you are about to commit. Files
convert as they are edited and the repo converges without a big-bang commit. On
a shallow clone with no base ref the gate skips rather than failing a build it
cannot scope; CI checks out with fetch-depth: 0 so it never has to.

That rule applies to this branch too, so the 87 files the ESLint commit
(e9c4b38) already touched are formatted here. None of them are files another
work stream owns — server.ts, lisa-client.ts, lisa-css.ts, birth.ts, cli*,
billing/**, log.ts and the assets tree are all untouched. .prettierignore keeps
out generated files, the vendored asset tree, website/ (own toolchain),
research/, packaging/, deploy/, contracts/, all Markdown (hand-formatted tables
and CJK spacing) and package*.json (npm owns those).

.editorconfig mirrors the same settings for editors that read it before Prettier
runs, with the two exceptions Prettier does not cover: Swift at 4 spaces and
Makefile tabs.

Verified: npm run typecheck, npm run lint (0 errors, 90 warnings — unchanged),
npm test (1,645 tests, 1,644 pass / 1 skipped / 0 fail), npm run build,
npm run format:check all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 5746f5759fc6b54aba77a684dc5774a30d5f240b)
…modules

T-4 (engineering gates): the repo had 1,645 tests and no way to see what they
miss. `npm run test:coverage` now runs the suite under c8 and emits text-summary
(console), lcov (tools) and json-summary (the gate below).

Measured baseline, 2026-09-06, lines / branches / functions:

  repo total              73.63 / 79.74 / 80.49
  src/billing/ (10 files) 84.89 / 80.49 / 90.91
  src/web/accounts.ts     94.50 / 89.55 / 97.44
  src/web/otp.ts          94.55 / 80.77 / 100.00
  src/web/sessions-auth.ts 95.56 / 78.79 / 100.00
  src/web/capabilities.ts 100.00 / 92.31 / 100.00
  src/soul/store.ts       78.17 / 85.88 / 67.86

Floors are min(85, measured) rounded down per metric, so the gate is green today
and can only be raised. c8's own --check-coverage could not express this: it is
either one global number (the repo is at 74%, and lifting that to 85% is a long
project, not a gate) or --per-file, which applies the same number to every one
of 284 files. scripts/coverage-thresholds.mjs reads coverage-summary.json and
checks the table instead — directory entries aggregate, file entries are exact,
and a target that stops matching any file fails rather than passing silently
after a rename.

The list is deliberately short: these are the paths where a coverage regression
means an untested branch in code that moves money, decides identity, or writes
Soul state. Raise a floor when real coverage passes it; adding tests is the only
correct way to make this gate pass.

CI runs coverage as its own job (the numbers do not vary across the Node matrix,
so running c8 three times would triple CI time for one report), uploads
coverage/lcov.info as an artifact, and the script writes the floor table to
$GITHUB_STEP_SUMMARY so the numbers are visible without downloading anything.

c8 is a devDependency; no runtime dependency added. coverage/ is gitignored.

Verified: npm run test:coverage green (1,645 tests, all floors met),
npm run lint 0 errors, npm run format:check green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 4d28a39cbb10f3f448a2d0827c2f5fc4540aae88)
T-5 (v0.24.0 tech review): 24 releases with no update bot left @anthropic-ai/sdk
32 minors behind and three advisories open in transitive deps (fast-uri high,
hono and qs moderate — all with fixes available).

Dependabot covers five manifests: the root npm package weekly, website/ weekly
(Astro deploys independently), packaging/gcp-relay monthly, github-actions
monthly, and packaging/mac-client (SwiftPM) monthly. The iOS companion is
XcodeGen with no SwiftPM dependencies, so there is nothing there to watch until
it grows a Package.swift.

Minor and patch updates are grouped into one PR per ecosystem. Ungrouped, a tree
this size produces a dozen PRs a week and the bot gets muted; grouped, the weekly
PR is one review and majors — which need real work in src/providers — still
arrive individually. @types/node majors are excluded: those track `engines` and
the CI Node matrix (20/22/24), and bumping them independently surfaces APIs the
supported runtimes do not have.

The CI audit job runs `npm audit --omit=dev --audit-level=high`. Production
dependencies only — dev-tree findings are build tooling that never ships and
never sees untrusted input, and gating on them makes the check noise. `high` is
the bar for the same reason: moderate advisories in transitive deps can sit
unfixable for weeks, and a permanently red gate teaches people to ignore it.

This gate is red at this commit — fast-uri's high advisory is still present. The
next commit in this series runs `npm audit fix`, which clears it; the gate is
added first so the fix commit has something proving it worked.

actionlint is unavailable in this environment (no Homebrew, and downloading a
binary to run is not something I will do unprompted), so both YAML files were
validated by parsing them with PyYAML and reviewed against the documented
schemas instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 267cb054ce72f3aa8bd3dd70c98458bc2f8a92e2)
T-4 (v0.24.0 tech review, §6.16 from v0.21 before it): PR CI was one job on one
Node version. website/, packaging/mac-client/ and packaging/ios-companion/ were
only ever built by the release pipeline, so a PR could break any of them and
nobody found out until a tag was cut.

ci.yml now has: `changes` (computes which surfaces a PR touched), `core` (the
Node matrix), `coverage`, `audit`, and three conditional jobs — `website`
(Astro build + page assertions), `macos` (swift build -c debug) and `ios`
(xcodegen + build.sh test on a simulator). Concurrency cancels superseded runs
per ref, except on main, whose runs are what release tags are cut from.

Path filtering is computed in a job rather than with `paths:`, which is
workflow-wide and cannot gate individual jobs. It diffs against the PR base (or
the push's `before`) and fails open — no usable base means run everything —
because a filter that silently skips a native build is worse than a slow run.
Editing ci.yml itself triggers all three, so a change to a job is proved by that
job and not by a follow-up commit.

The iOS job discovers the simulator instead of hardcoding one: build.sh defaults
to "iPhone 17 Pro", and pinning a device name in CI breaks the day GitHub rolls
the runner image. It asks the installed Xcode for its available iPhones and
fails loudly, with the device list, if there are none.

TWO FINDINGS, both fixed here:

1. `npm test` did not work on Node 20 at all. The script passed the glob
   "src/**/*.test.ts" to `node --test`, which only expands globs on Node 22+;
   Node 20 printed «Could not find 'src/**/*.test.ts'» and exited 1. scripts/
   run-tests.mjs walks src/ and passes explicit paths, so one command works
   across the matrix and does not depend on sh vs cmd globbing. Extra args still
   forward (`npm test -- --test-name-pattern=soul`).

2. With that fixed, Node 20 fails 26 tests: undici 8.9.0 — a *production*
   dependency — declares `engines: node >=22.19.0`, and its webidl layer calls
   worker_threads' markAsUncloneable, absent before Node 22.10. Verified on
   20.20.2. So `engines: >=20.0.0` was already a false promise: installing on
   Node 20 succeeds and then dies at runtime inside undici.

   `engines` is now >=22.19.0, matching undici, the CI matrix (22/24) and
   @types/node ^22. This is user-visible — `npm i -g @oratis/lisa` on Node 20
   now fails at install with a clear message instead of at runtime with a
   confusing one — and it is the honest version of what the package already
   required. src/cli/doctor.ts:48 still prints "need ≥ 20"; that file belongs to
   another work stream right now and is left for them.

actionlint could not be installed here (no Homebrew in this environment), so the
workflow was validated by parsing it with PyYAML, and the two non-trivial shell
snippets — the path filter and the simulator picker — were executed locally
against synthetic inputs covering src-only, native-only, ci.yml and empty diffs.

The job formerly called `check` is now `core`; branch protection's required
checks need updating to match.

Verified: npm run typecheck, npm run lint (0 errors, 90 warnings),
npm run format:check, npm test (1,645 tests, 1,644 pass / 1 skip / 0 fail),
npm run build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 4a2a06bd7c3448afc80ed3ebf6092ceb56225694)
T-4 / UX-2 (v0.24.0 reviews): RELEASE_v0.23 described a "real-browser E2E" pass
that never landed in the repo — `playwright|puppeteer` had zero hits. The first
run, the birth ritual and the mobile breakpoints were all verified by hand, once,
by a human reading a browser.

23 specs, ~19s, zero model calls, zero network:

  key-gate   an unconfigured home shows the key gate and NOT the birth ritual
  main-shell identity card / 3x3 nav / session tree / composer; view switching;
             theme toggle persisting lisa-theme across a reload; +New adding a
             tree leaf
  layout     375x812, 768x1024, 1440x900 x rail collapsed/expanded — no
             horizontal page scroll, all shell regions have real width
  birth      401 → the ritual surfaces a failure and leaves isBorn() false;
             success → every step streams through to "done", ENTER lands in the
             chat view, and the dreamed soul is on disk

Determinism comes from three pieces:

- helpers/stub-anthropic.ts stands in for api.anthropic.com. The SDK honours
  ANTHROPIC_BASE_URL (registry.ts passes it through), so pointing that at a
  local stub is the whole trick. It emits the exact event sequence
  MessageStream needs — message_start, content_block_start, two text_deltas
  (a split payload, so the accumulator is actually exercised),
  content_block_stop, message_delta, message_stop — or a 401
  authentication_error in the shape the real API returns.
- helpers/make-soul.ts fabricates a born soul through src/soul/store.ts, in
  birth.ts's exact write order: everything else first, seed.json last (it is
  the isBorn() flip), then the lock. It runs as its own tsx process because
  soul paths resolve LISA_HOME at call time.
- helpers/lisa-server.ts gives each spec file a throwaway LISA_HOME *and* HOME
  under .tmp/e2e, a free port, and `node dist/cli.js serve --web --no-idle
  --no-reflect --no-mcp --no-plugins`. A separate HOME matters: it is what
  keeps the claude-code watcher off the operator's real ~/.claude. GIT_AUTHOR_*
  and GIT_COMMITTER_* are set because the soul store commits on every write and
  a temp HOME has no ~/.gitconfig.

The global setup only builds — dist/, not tsx, because a smoke test that passes
against the dev loader and not the shipped artefact is worth nothing. Servers
are per spec file: the four scenarios need four different homes (no soul + no
key, fabricated soul, empty home + failing stub, empty home + working stub), so
one shared instance cannot serve them.

FOUR ASSERTIONS ARE test.fixme, all describing behaviour another stream is
fixing right now, all verified to fail against this worktree today:

  UX-2 · .main is 75px at 375px wide with the rail collapsed (expected 375) —
         exactly the number the UX review measured — and with the rail open the
         send button ends at x=665, off a 375px screen.
  UX-1 · #birthError renders `401 {"type":"error","error":{"type":
         "authentication_error",…}}` verbatim, and there is no Change key
         button anywhere in the DOM.

Flip those four from test.fixme to test after the UX fixes integrate; they are
the acceptance criteria for UX-1 and UX-2 written down.

Chromium only (this is a localhost app, not a public website — a three-browser
matrix triples the slowest CI job for little), retries 1 on CI, trace on first
retry. CI installs the browser with --with-deps, typechecks the specs first
(tests/e2e/tsconfig.json exists because these are the only files in the repo
that are both Node and DOM), and uploads the HTML report on failure.

Verified: full suite 23 passed / 4 skipped in 19s; npm run typecheck,
npm run typecheck:e2e, npm run lint (0 errors, 90 warnings),
npm run format:check, npm test (1,645) and npm run build all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 671040fa5e6ec0fae7d8bcd9830ac001217fccb5)
… 6.49

T-5 wave 1, step 1 of 3. `npm audit fix` clears all three advisories the review
found — fast-uri (high, SSRF-adjacent), hono and qs (moderate), all transitive
through @modelcontextprotocol/sdk. `npm audit --omit=dev` now reports 0
vulnerabilities, so the CI audit gate added two commits ago goes green.

Minor/patch updates:
  @google/genai              2.13.0 → 2.21.0
  @modelcontextprotocol/sdk   1.29.0 → 1.30.0
  imapflow                     1.4.2 → 1.7.8
  music-metadata             11.14.0 → 11.15.0
  openai                      6.35.0 → 6.49.0
  sharp                       0.35.3 → 0.35.4
  tsx                         4.23.1 → 4.23.13
  undici                       8.9.0 → 8.10.2
  @types/node               22.19.17 → 22.20.1  (stays on 22.x: it tracks the
                                                 CI matrix floor, not the newest
                                                 Node)

openai 6.49 forced one source change. src/voice/transcribe.ts passed
`fs.createReadStream(audioPath)` to audio.transcriptions.create; the new SDK
does not consume the stream before the request settles, so the ReadStream's
async open landed after the test had deleted its temp file — «generated
asynchronous activity after the test ended … ENOENT». The fix is not a test
workaround: a ReadStream that nothing consumes leaks its descriptor, so any
failed transcription request leaked an fd. Reading the clip and handing the SDK
a File via toFile() removes both the race and the leak, and clips are already
length-capped by maxTranscriptionSeconds() so buffering one is bounded.

Also de-flaked src/mood-bus.test.ts: the mirror-file poll was 50 iterations of
10ms, which loses the race under `npm run test:coverage` where c8's
instrumentation slows every write. It now polls to a 10s deadline. Caught by
running the coverage job, not the plain suite — which is the point of having it.

Verified on BOTH runtimes in the CI matrix: npm test 1,645 tests, 1,644 pass /
1 skip / 0 fail on Node 24.12 and on Node 22 (`npx -y -p node@22 npm test`).
Plus npm run typecheck, npm run lint (0 errors, 90 warnings),
npm run format:check, npm run build, npm run test:coverage (all floors met) and
the Playwright suite (23 passed / 4 fixme).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit d9fc6d0b99a3052a11e7de33f91e5d0680ccd9c7)
… forced

T-5 wave 1, step 2 of 3. The SDK was 32 minors behind — new model and tool
capabilities were reachable only by casting around types that predated them.

The upgrade is source-compatible: typecheck, the 54 provider tests and the full
1,645-test suite pass unchanged. What changed is that three workarounds are no
longer needed, and lint's no-unnecessary-type-assertion count drops from 90 to
88 warnings on its own.

Removed:

- `output_config.effort` was written through a double cast
  (`(params as { output_config?: { effort?: string } })`) because 0.92 had no
  such field. It is now `OutputConfig` on MessageCreateParams with `effort`
  typed as 'low'|'medium'|'high'|'xhigh'|'max' — the same union
  ProviderRunOpts already declares — so it is a plain assignment. The Haiku
  gate (modelSupportsEffort) stays: that is an API behaviour, not a type gap.
- The compaction extras were `{ betas?: string[]; context_management?: object }`
  — a hand-written shape standing in for types that did not exist. They are now
  `Pick<Anthropic.Beta.Messages.MessageCreateParamsStreaming, "betas" |
  "context_management">`, so a wrong beta name or a malformed edit is a compile
  error instead of a 400 at runtime.
- StreamLike.finalMessage() returned `Promise<unknown>`; it now returns
  `Message | BetaMessage`, which is what the two endpoints actually return.

Kept, with the reason written down: the `as Anthropic.Message` at the end of
runTurn. BetaMessage's content is a superset of Message's, and ProviderResult
declares Anthropic.ContentBlock[] — that narrowing is inherent to supporting
both the beta (compaction) and stable endpoints from one code path, not a
leftover. Streaming, tool use, thinking, effort, the compaction beta and abort
all keep working; the existing tests cover each.

Also de-flaked src/mood-bus.test.ts properly. Raising its poll budget (previous
commit) was the wrong diagnosis: it waited the full 10s and still saw nothing,
because persist() is best-effort by design and swallows every error — under a
loaded full-suite run its single fire-and-forget write can be dropped outright,
and no amount of waiting conjures the file. The test now re-issues the same set
on each pass, which is exactly what the production path does (memory is the
source of truth, the next set re-persists), so it asserts the mirroring
behaviour instead of one syscall's luck. Three consecutive full-suite runs
green.

Verified: npm test 1,645 / 1,644 pass / 1 skip / 0 fail on Node 24 AND Node 22;
npm run typecheck, lint (0 errors, 88 warnings — down from 90), format:check,
build, test:coverage (all floors met) and Playwright (23 passed / 4 fixme).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 88bc9a60b7a77dbbf6436fbbe2b963eea1010d5a)
T-5 wave 1, step 3 of 3 (the first of the two stretch upgrades). Kept because
every gate is green with no source change at all: typecheck, npm run lint
(0 errors, 88 warnings — unchanged), npm test 1,645 / 1,644 pass / 1 skip / 0
fail on BOTH Node 24 and Node 22, npm run build, the Playwright suite (23 passed
/ 4 fixme) and npm audit --omit=dev (0 vulnerabilities).

src/providers/openai.ts and src/voice/transcribe.ts are the only consumers and
neither needed touching — the 6.49 work in the first commit of this series
(toFile instead of a ReadStream) had already moved transcribe off the API that
was going to be the sharp edge.

openai 7 declares `engines: node >=22.0.0`, which is consistent with this
package's own floor of >=22.19.0 (set by undici).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 1ae2ef1de60bec0755c602d459a3a07f4a2710c5)
T-5 wave 1, the second stretch upgrade: attempted, measured, and rejected. The
finding is recorded where it is actionable — Dependabot will not open a
TypeScript major PR that cannot merge.

TypeScript 7.0.2 is ready on the language side. Measured on this tree:

  npm run typecheck   green
  npm run build       green
  dist/ equivalence   284 .js and 284 .d.ts files, byte-identical to the 5.9.3
                      output. The only 161 differing files are *.js.map (108)
                      and *.d.ts.map (53) — source-map mappings, expected from a
                      different compiler emitting the same semantics.

It is blocked on tooling, not on this codebase: typescript-eslint 8 refuses to
load at all against TS 7 —

  Error: typescript-eslint does not support TS 7.0.
    at node_modules/typescript-eslint/dist/index.js:52

— so `npm run lint` dies before linting a single file. npm also has to override
a peer dependency to install the pair. Trading the linter this branch just
introduced for a compiler that emits identical output is a bad deal, so
TypeScript stays on ^5.7.0 (5.9.3 resolved).

Revisit when typescript-eslint ships TS >=7 support
(typescript-eslint/typescript-eslint#10940); the alternative, running
typescript-eslint against a side-by-side TS 6 install, is more moving parts than
this buys today.

Verified after reverting: npm run typecheck, npm run lint (0 errors, 88
warnings), npm run format:check, npm test (1,645 / 1,644 pass / 1 skip / 0 fail),
npm run build, npm audit --omit=dev (0 vulnerabilities) — all green, and
package-lock.json is back to exactly what `npm ci` installs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 2a40d021b666c7f6df9cb320a5fb89807eb0f98d)
The comment justifying the absence of Node 20 named undici 8.9; the
audit-fix commit moved it to 8.10.2. The constraint is unchanged — every
8.x declares engines >=22.19.0 — but a comment that cites a version
should cite the one in the lockfile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit a0e70629909847bf31b9d6c219a37d9a5fe5b0ee)
Each stream was green on its own branch, but the ESLint baseline was computed
against a tree that did not yet contain the other seven streams' new files.
Integrated, 37 errors appeared in code none of the streams could have linted.
Twelve were mechanical (`--fix`); the rest are here, by kind:

**`no-console` in src/billing/reconcile.ts (11).** `cmdBillingReconcile` moved
to src/cli/billing-reconcile.ts. Everything it does beyond calling
reconcileOnce() is printing, and a reconciler that writes to stdout from inside
library code cannot be called from a request handler or a timer without
polluting the log — which is precisely why src/billing keeps `no-console` at
error. The library half keeps its structured logInfo/logError calls.

**Empty catches (6).** Each now states the condition it is swallowing: a
double close during log rotation, a missing rotation generation, a log file
moved out from under us, a directory without .git while walking up, an
unresolvable argv[0]. Matching the house style of explaining why, not what.

**ANSI escapes in regexes (4).** src/cli/render.test.ts asserts on real
`\x1b[…m` sequences because emitting or suppressing them is the thing under
test. Rule disabled for that file with the reason.

**`any` in types/web-client.d.ts (4).** The ambient file exists so
`tsc --checkJs` can run over the extracted client bundle; its loose index
signatures are the mechanism, not an oversight. Rule disabled with the reason,
and the file joined the lint program (tsconfig.eslint.json) so everything else
in it is still checked.

**Dead references (3).** `capabilityProfileForEdition` (superseded by the
per-surface profiles), `lastPromptFingerprintIn` (orphaned by the streamed
readMessagePage rewrite), and an unused test parameter.

Also merged three overlapping edits the streams made to the same lines:
package.json keeps all four new scripts; src/tools/registry.ts keeps the
cast-free list plus the archived doc path; src/channels/router.ts keeps
`sandboxModeForProfile` over the reformatted older call; src/sessions/store.ts
keeps the streamed bounded-ring page reader over the reformatted readFile one;
and the extracted client bundle carries the docs/archive/plans/ paths the docs
stream fixed in the template literals it replaced.

Verified on the integrated tree: typecheck, typecheck:client, lint (0 errors,
69 warnings — all pre-existing baseline entries), check:api-contract, build
with no test files in dist, and 1,951 tests passing (0 failures, 1 PTY skip).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
oratis and others added 3 commits September 7, 2026 12:24
The knowledge base should carry the outcome, not just the diagnosis: which PR
holds which theme, the verification numbers on the integrated tree, and — the
part worth remembering — the three defects that only appeared once the eight
streams were in one tree.

The birth-timer one is the instructive case. Every stream was green on its own
branch; the bug needed the Node 20/22/24 matrix, which arrived in a different
stream, to become visible at all. That is the argument for the matrix, and the
reason this file now says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The format gate compares against origin/main and requires every changed file to
be Prettier-clean. Eight streams wrote these 62 files in parallel worktrees, all
of them before the Prettier config existed on any branch, so the gate failed the
moment it first ran on the integrated tree — which is the gate working.

It is one commit at the end of the chain rather than one per stream on purpose.
Formatting a file on an early branch that a later branch then rewrites (server.ts
and the client are edited by three streams between them) buys nothing but rebase
conflicts, and the whole chain lands together anyway. src/web/assets/ is in
.prettierignore, so the extracted client bundle is untouched and the byte-level
HTML composition tests still hold.

Formatting only: no behaviour changed. typecheck, lint, check:api-contract,
build and 1,951 tests are green after it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…th about Node

Three things the integrated tree exposed that no single branch could.

**The key-gate specs tested markup that no longer exists.** They were written
against `#cfgAnthropic`, the Anthropic-only field; the web-UI branch replaced
the gate with a provider picker (`#cfgProvider` + `#cfgKey`) in the same chain.
Retargeted, and while there, the spec now also asserts the picker is actually
populated — an empty <select> would leave a first-time user with a key field
labelled for a provider they may not have an account with, which is the dead
end UX-6 set out to remove.

**Four test.fixme assertions are now live.** They were parked because the
behaviour they describe lived on a branch the tooling stream could not see:
the two UX-2 mobile assertions (.main fills the viewport, SEND is on screen at
375px) and the two UX-1 birth assertions (a human error, a Change key button
back to the gate). All four fixes landed earlier in this chain, so the tests
are enabled — and they are what stops those two P0s from coming back. 27/27
green locally.

**`engines` moved to >=22.19.0 but three places still promised Node 20.**
The floor moved for a real reason (undici calls worker_threads APIs added in
22.10, so `>=20.0.0` was a false promise that failed at runtime instead of at
install). Left unaligned, a Node 20 user read "Node ≥ 20" in the README or on
the install page, ran npm i, and got refused. `lisa doctor` was worse: its
check *passed* on Node 20, so the tool told them their environment was fine
minutes before it broke. The check now tests major.minor, and the README, the
Chinese README and both install pages say 22.19.

Verified: typecheck, lint, format:check, 1,951 unit tests, 27 e2e, website.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant